import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
  BaseAddress,
  BaseKey,
  BaseTransaction,
  BaseTransactionBuilder,
  NotSupported,
  TransactionType,
} from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import {
  Transaction as SOLTransaction,
  Message as SOLMessage,
  SystemProgram,
  SystemInstruction,
  StakeProgram,
} from '@solana/web3.js';

import assert from 'assert';
import BigNumber from 'bignumber.js';
import { nonceAdvanceInstruction, validInstructionData, validInstructionData2 } from './constants';

export class StakingRawMsgAuthorizeBuilder extends BaseTransactionBuilder {
  protected _transaction: Transaction;
  protected _transactionMessage: string;
  constructor(_coinConfig: Readonly<CoinConfig>) {
    super(_coinConfig);
    this._transaction = new Transaction(_coinConfig);
  }

  protected get transactionType(): TransactionType {
    return TransactionType.StakingAuthorizeRaw;
  }

  /** @inheritdoc */
  initBuilder(tx: Transaction): void {
    if (this.validateTransaction(tx)) {
      this.transactionMessage(tx.solTransaction.serializeMessage().toString('base64'));
    }
  }

  /**
   * The raw message generated by Solana CLI.
   *
   * @param {string} msg msg generated by 'solana stake-authorize-check.
   * @returns {StakeBuilder} This staking builder.
   *
   */
  transactionMessage(msg: string): this {
    this.validateMessage(msg);
    this._transactionMessage = msg;
    return this;
  }

  /** @inheritdoc */
  protected async buildImplementation(): Promise<Transaction> {
    assert(this._transactionMessage, 'missing transaction message');

    this.validateMessage(this._transactionMessage);
    const solTransaction = SOLTransaction.populate(
      SOLMessage.from(Buffer.from(this._transactionMessage, 'base64')),
      []
    );
    // this is workaround for solana web3.js generate wrong signing message
    const serialized = solTransaction.serialize({ requireAllSignatures: false }).toString('base64');
    this.transaction.fromRawTransaction(serialized);
    this.transaction.setTransactionType(this.transactionType);
    assert(this._transactionMessage === this.transaction.signablePayload.toString('base64'), 'wrong signing message');
    return this.transaction;
  }

  validateTransaction(tx: Transaction): boolean {
    return this.validateMessage(tx.solTransaction.serializeMessage().toString('base64'));
  }

  async build(): Promise<Transaction> {
    return this.buildImplementation();
  }

  protected validateMessage(msg: string): boolean {
    const tx = SOLTransaction.populate(SOLMessage.from(Buffer.from(msg, 'base64')), []);
    const instructions = tx.instructions;
    if (instructions.length !== 2 && instructions.length !== 3) {
      throw new Error(`Invalid transaction, expected 2 instruction, got ${instructions.length}`);
    }
    for (const instruction of instructions) {
      switch (instruction.programId.toString()) {
        case SystemProgram.programId.toString():
          const instructionName = SystemInstruction.decodeInstructionType(instruction);
          if (instructionName !== nonceAdvanceInstruction) {
            throw new Error(`Invalid system instruction : ${instructionName}`);
          }
          break;
        case StakeProgram.programId.toString():
          const data = instruction.data.toString('hex');
          if (data !== validInstructionData && data !== validInstructionData2) {
            throw new Error(`Invalid staking instruction data: ${data}`);
          }
          break;
        default:
          throw new Error(
            `Invalid transaction, instruction program id not supported: ${instruction.programId.toString()}`
          );
      }
    }
    return true;
  }

  protected fromImplementation(rawTransaction: string): Transaction {
    const tx = new Transaction(this._coinConfig);
    tx.fromRawTransaction(rawTransaction);
    this.initBuilder(tx);
    return this.transaction;
  }

  protected signImplementation(key: BaseKey): BaseTransaction {
    throw new NotSupported('Method not supported on this builder');
  }

  protected get transaction(): Transaction {
    return this._transaction;
  }

  validateAddress(address: BaseAddress, addressFormat?: string): void {
    throw new NotSupported('Method not supported on this builder');
  }

  validateKey(key: BaseKey): void {
    throw new NotSupported('Method not supported on this builder');
  }

  validateRawTransaction(rawTransaction: string): void {
    const tx = new Transaction(this._coinConfig);
    tx.fromRawTransaction(rawTransaction);
    this.validateTransaction(tx);
  }

  validateValue(value: BigNumber): void {
    throw new NotSupported('Method not supported on this builder');
  }
}
